import re
import logging
from .utils import *


class DeviceTree():
    def __init__(self, ignore_pci_device):
        self.devices = list()
        self.drivers = set()
        self.ignore_pci_device = ignore_pci_device
        self.build_device_tree()

    def build_device_tree(self):
        _, output = run_local_cmd('udevadm info --export-db', 'Failed to get device info!')
        for item in output.split('\n\n'):
            props = dict()
            for line in item.split('\n'):
                if line.startswith('P:'):
                    props.update({'SYSPATH': str(line.split(': ', 1)[1].strip())})
                elif line.startswith('E:'):
                    attr = line.split(': ', 1)[1].strip().split('=', 1)
                    props.update({str(attr[0]): str(attr[1])})
                    if attr[0] == 'DRIVER':
                        self.drivers.add(attr[1])
                elif line.startswith('N:'):
                    props.update({'NAME': str(line.split(': ', 1)[1].strip())})
            if self.ignore_pci_device:
                if 'PCI_SLOT_NAME' in props:
                    if props['PCI_SLOT_NAME'] in self.ignore_pci_device:
                        logging.warning('skip pci device %s' % props['PCI_SLOT_NAME'])
                        continue
            if props:
                self.devices.append(Device(props))


class Device():
    pci_endpoint_pattern = r'\b([0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9a-fA-F]{1}$)'
    nvme_virtual_device = r'/devices/virtual/nvme-subsystem/nvme-subsys\d{1,2}/(nvme\d{1,2})n\d{1,2}'

    def __init__(self, props):
        self.properties = props
        self.name = (self.get_property('ID_VENDOR_FROM_DATABASE') + ' ' + \
            self.get_property('ID_MODEL_FROM_DATABASE')).strip()
        self.driver = self.get_property('DRIVER')
        self.syspath = self.get_property('SYSPATH')
        self.classcode = fixed_length(self.get_property('PCI_CLASS')) \
            if self.get_property('PCI_CLASS') else ''
        self.vid, self.did = self.get_property('PCI_ID').split(':') \
            if self.get_property('PCI_ID') else ['', '']
        self.subvid, self.subdid = self.get_property('PCI_SUBSYS_ID').split(':') \
            if self.get_property('PCI_SUBSYS_ID') else ['', '']
        self.properties.update({'DEVICE_NAME': self.name})
        self.module = get_module_name(self.driver) if self.driver else self.driver
        logging.debug('device %s: vendor name %s, model name %s' % (self.syspath, \
                self.get_property('ID_VENDOR_FROM_DATABASE'), \
                self.get_property('ID_MODEL_FROM_DATABASE')))

    def get_property(self, key):
        return self.properties.get(key, '')

    def has_property(self, prop):
        return prop in self.properties

    def is_pci_endpoint(self):
        if re.findall(Device.pci_endpoint_pattern, self.syspath):
            return True
        return False

    def is_nvme_virtual_device(self):
        group = re.match(Device.nvme_virtual_device, self.syspath)
        if group:
            return True, group.group(1)
        return False, ''

    @property
    def attributes(self):
        return set()


class FakeDevice():
    def __init__(self, name):
        self.properties = {}
        self.name = name

    def __getattr__(self, name):
        if name in ['module', 'driver', 'syspath', 'classcode', 'vid', 'did', 'subvid', 'subdid']:
            return ''
        return object.__getattr__(self, name)

    def get_property(self, key):
        return 'fake device'


class DevDisk():
    def __init__(self, device_inst, ctr_inst, stor_inst):
        self.device_inst = device_inst
        self.dev_path = self.device_inst.get_property('DEVNAME')
        self.partition_inst = []
        self.mount_point = ''
        self.mount_point_free_space = 0 #GB
        self.is_boot_disk = stor_inst.boot_disk.startswith(self.dev_path)
        if self.is_boot_disk:
            ctr_inst.is_boot_controller = True
            self.properties.update({'IS_BOOT_DISK': 'yes'})
        self.is_pvs_disk = is_pvs_disk(self.dev_path, stor_inst.pvs_disk)
        self.check_mount_point()
        self.properties.update({'DISK_TYPE': get_disk_type(self.dev_path)})
        self.properties.update({'IS_RAW_DISK': 'false'})
        if not self.is_partition:
            self.properties.update({'DISK_TRANSPORT': get_disk_transport(self.dev_path)})
        if stor_inst.name not in ['nvme']:
            self.properties.update({'DISK_DRIVER': get_disk_driver(self.dev_path)})
            self.properties.update({'DISK_VENDOR': get_disk_vendor(self.syspath)})

    def __getattr__(self, name):
        try:
            return getattr(self.device_inst, name)
        except AttributeError as e:
            print(e)
            raise AttributeError('{0} object has no parameter {1}'.format(type(self).__name__,
                                                                          name))

    @property
    def is_partition(self):
        if self.device_inst.has_property('PARTN'):
            if self.device_inst.get_property('PARTN'):
                return True
            else:
                return False
        else:
            if self.device_inst.get_property('DEVTYPE') in ['partition']:
                return True
            else:
                return False

    @property
    def attributes(self):
        attrs = set()
        if not self.is_partition and not self.partition_inst and not self.is_boot_disk:
            attrs.add('raw')
        return attrs

    def filter_partition(self):
        if not self.partition_inst:
            return
        if not self.is_partition:
            logging.debug('%s should not have partition' % self.dev_path)
        for _ in range(len(self.partition_inst)):
            dk = self.partition_inst.pop(0)
            if dk.mount_point_free_space >= 10:
                self.partition_inst.append(dk)
        if not self.partition_inst:
            logging.debug('%s dose not have available partition' % self.dev_path)
        self.partition_inst = sorted(self.partition_inst, reverse=True, key=lambda d: d.mount_point_free_space)
        logging.debug('disk %s has %s available partition' % (self.dev_path, len(self.partition_inst)))
        logging.debug('partitions are: %s' % ','.join([p.dev_path for p in self.partition_inst]))

    def check_mount_point(self):
        # TODO: detect lvm format partition
        if self.is_boot_disk:
            self.mount_point = '/'
            self.mount_point_free_space = get_mount_point_free_space('/')
        elif self.is_partition:
            self.mount_point = get_mount_point(self.dev_path)
            if self.mount_point:
                self.mount_point_free_space = get_mount_point_free_space(self.mount_point)
        else:
            pass

class Controller():
    def __init__(self, device_inst, component):
        self.valid = False
        self.component = component
        self.device_inst = device_inst
        # TODO: should record unavailable devices, then deplay to tester
        # here is the available devices
        self.devices = list()
        self.available_devices = list()
        self.bus_device = self.syspath.rsplit('.', 1)[0]
        self.bdf = self.syspath.rsplit('/')[-1]
        self.device_inst.properties.update({'PCI_BDF': self.bdf.split(':', 1)[-1]})
        self.test_mode = self.component.env.get('ANCERT_SYSTEM_TEST_MODE', '')
        self.is_raid = is_sub_class_code(['000104'], self.classcode)
        self.is_boot_ctr = False
        if not self.device_inst.name or not self.device_inst.get_property('ID_MODEL_FROM_DATABASE'):
            if self.component.name not in ['cpu', 'memory']:
                self.device_inst.name = get_device_name_from_hwdb(self)
                if not self.device_inst.name:
                    self.device_inst.name = '%s' % self.device_inst.get_property('PCI_ID')
                    if self.device_inst.get_property('PCI_SUBSYS_ID'):
                        self.device_inst.name += '[%s]' % self.device_inst.get_property('PCI_SUBSYS_ID')
                    if self.device_inst.get_property('PCI_CLASS'):
                        self.device_inst.name += '[%s]' % self.device_inst.get_property('PCI_CLASS')

    def __getattr__(self, name):
        try:
            return getattr(self.device_inst, name)
        except AttributeError as e:
            print(e)
            raise AttributeError('{0} object has no parameter {1}'.format(type(self).__name__,
                                                                          name))

    def sort_devices(self):
        self.devices = sorted(self.devices, key=lambda d: d.name)

    # Network controller may have 1/2/4 nic devices
    def pickup_device(self, index=0):
        logging.debug('select index %s for testing from available list' % index)
        logging.debug('%s was selected for testing' % self.available_devices[index].name)
        return self.available_devices[index]

    def update(self):
        self.device_inst.properties['ALL_DEVICES_OF_CONTROLLER'] = []
        for dev in self.devices:
            if dev is self.device_inst:
                continue
            self.device_inst.properties['ALL_DEVICES_OF_CONTROLLER'].append(dev.properties)

    @property
    def devinfo(self):
        info = []
        info.append(self.name)
        info.append(self.driver)
        info.append(self.module)
        info.append(self.classcode)
        info.append(self.vid)
        info.append(self.did)
        info.append(self.subvid)
        info.append(self.subdid)
        info.append(self.syspath)
        return '|'.join(info)


class StorageController(Controller):
    def __init__(self, device_inst, component):
        super(StorageController, self).__init__(device_inst, component)
        self.is_boot_controller = False
        self.raid_controller_id = ''

    def set_valid(self, con):
        if not self.get_property('DRIVER'):
            return
        if self.is_boot_controller:
            logging.debug('%s is boot controller' % self.name)
        if self.is_boot_controller and con.is_system_test:
            self.valid = True
        else:
            for dk in self.available_devices:
                if dk.is_partition:
                    continue
                if dk.partition_inst:
                    continue
                self.valid = True

    def pickup_device(self, index=0):
        logging.debug('select index %s for testing from available list' % index)
        dk = self.available_devices[index]
        dk.properties.update({'STORAGE_TESTED_DISK': 'yes'})
        if dk.partition_inst:
            logging.debug('partition %s was selected for storage testing' % dk.partition_inst[0].dev_path)
            return dk.partition_inst[0]
        logging.debug('disk %s was selected for storage testing' % dk.dev_path)
        return dk
